Micron Document
🎖️GitЯра🎖️

Node / meshtastic / Meshtastic-Android / files / .github / workflows / update-changelog.yml

Displaying Raw • Download

.github/workflows/update-changelog.yml claude/packet-authenticity-2-8-0-5b07c8 (74b6aac9) Text, 11.53 KB

Tff7b72nameTb4b4b4: Ta5d6ffUpdateTa5d6ff Ta5d6ffChangelog

Tff7b72onTb4b4b4:
Tff7b72pushTb4b4b4:
Tff7b72branchesTb4b4b4:
Tb4b4b4- Ta5d6ffmain
Tff7b72paths-ignoreTb4b4b4:
Tb4b4b4- Ta5d6ff'Ta5d6ffCHANGELOG.mdTa5d6ff'

Tff7b72permissionsTb4b4b4:
Tff7b72contentsTb4b4b4: Ta5d6ffwrite
Tff7b72pull-requestsTb4b4b4: Ta5d6ffwrite
Tff7b72statusesTb4b4b4: Ta5d6ffwrite

Tff7b72concurrencyTb4b4b4:
Tff7b72groupTb4b4b4: Ta5d6ffchangelog-${{Ta5d6ff Ta5d6ffgithub.refTa5d6ff Ta5d6ff}}
Tff7b72cancel-in-progressTb4b4b4: Ta5d6fftrue

Tff7b72jobsTb4b4b4:
Tff7b72update-changelogTb4b4b4:
Tff7b72runs-onTb4b4b4: Ta5d6ffubuntu-24.04-arm
Tff7b72stepsTb4b4b4:
Tb4b4b4- Tff7b72nameTb4b4b4: Ta5d6ffCheckoutTa5d6ff Ta5d6ffcode
Tff7b72usesTb4b4b4: Ta5d6ffactions/checkout@v7.0.1
Tff7b72withTb4b4b4:
Tff7b72refTb4b4b4: Ta5d6ffmain
Tff7b72fetch-depthTb4b4b4: Ta5d6ff0

Tb4b4b4- Tff7b72nameTb4b4b4: Ta5d6ffDiscoverTa5d6ff Ta5d6ffchannelTa5d6ff Ta5d6fftags
Tff7b72idTb4b4b4: Ta5d6fftags
Tff7b72runTb4b4b4: Tb4b4b4|
Tff7b72# Find the latest production tag (vX.Y.Z)
Tff7b72PROD=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
Tff7b72echo "prod=${PROD:-}" >> "$GITHUB_OUTPUT"

Tff7b72if [ -z "$PROD" ]; then
Tff7b72echo "No production tag found. Skipping."
Tff7b72exit 0
Tff7b72fi

Tff7b72# Find the latest tag for each channel
Tff7b72INTERNAL=$(git tag --list 'v*-internal.*' --sort=-v:refname | head -1)
Tff7b72CLOSED=$(git tag --list 'v*-closed.*' --sort=-v:refname | head -1)
Tff7b72OPEN=$(git tag --list 'v*-open.*' --sort=-v:refname | head -1)

Tff7b72# Deduplicate: if multiple channel tags point to the same SHA,
Tff7b72# keep only the highest channel (open > closed > internal).
Tff7b72# This happens when a build is promoted between channels.
Tff7b72PROD_SHA=$(git rev-parse "$PROD" 2>/dev/null)
Tff7b72INTERNAL_SHA=$([ -n "$INTERNAL" ] && git rev-parse "$INTERNAL" 2>/dev/null || echo "")
Tff7b72CLOSED_SHA=$([ -n "$CLOSED" ] && git rev-parse "$CLOSED" 2>/dev/null || echo "")
Tff7b72OPEN_SHA=$([ -n "$OPEN" ] && git rev-parse "$OPEN" 2>/dev/null || echo "")

Tff7b72# Open subsumes closed and internal at the same SHA
Tff7b72if [ -n "$OPEN_SHA" ]; then
Tff7b72[ "$CLOSED_SHA" = "$OPEN_SHA" ] && CLOSED="" && CLOSED_SHA=""
Tff7b72[ "$INTERNAL_SHA" = "$OPEN_SHA" ] && INTERNAL="" && INTERNAL_SHA=""
Tff7b72fi
Tff7b72# Closed subsumes internal at the same SHA
Tff7b72if [ -n "$CLOSED_SHA" ]; then
Tff7b72[ "$INTERNAL_SHA" = "$CLOSED_SHA" ] && INTERNAL="" && INTERNAL_SHA=""
Tff7b72fi
Tff7b72# Drop any channel tag that points to the production SHA
Tff7b72[ "$INTERNAL_SHA" = "$PROD_SHA" ] && INTERNAL="" && INTERNAL_SHA=""
Tff7b72[ "$CLOSED_SHA" = "$PROD_SHA" ] && CLOSED="" && CLOSED_SHA=""
Tff7b72[ "$OPEN_SHA" = "$PROD_SHA" ] && OPEN="" && OPEN_SHA=""

Tff7b72echo "internal=${INTERNAL:-}" >> "$GITHUB_OUTPUT"
Tff7b72echo "closed=${CLOSED:-}" >> "$GITHUB_OUTPUT"
Tff7b72echo "open=${OPEN:-}" >> "$GITHUB_OUTPUT"

Tff7b72echo "Tags after dedup: prod=$PROD internal=$INTERNAL closed=$CLOSED open=$OPEN"

Tb4b4b4- Tff7b72nameTb4b4b4: Ta5d6ffGenerateTa5d6ff Ta5d6ffsegmentedTa5d6ff Ta5d6ffchangelog
Tff7b72ifTb4b4b4: Ta5d6ffsteps.tags.outputs.prodTa5d6ff Ta5d6ff!=Ta5d6ff Ta5d6ff''
Tff7b72envTb4b4b4:
Tff7b72GH_TOKENTb4b4b4: Ta5d6ff${{Ta5d6ff Ta5d6ffgithub.tokenTa5d6ff Ta5d6ff}}
Tff7b72REPOTb4b4b4: Ta5d6ff${{Ta5d6ff Ta5d6ffgithub.repositoryTa5d6ff Ta5d6ff}}
Tff7b72HEAD_SHATb4b4b4: Ta5d6ff${{Ta5d6ff Ta5d6ffgithub.shaTa5d6ff Ta5d6ff}}
Tff7b72PROD_TAGTb4b4b4: Ta5d6ff${{Ta5d6ff Ta5d6ffsteps.tags.outputs.prodTa5d6ff Ta5d6ff}}
Tff7b72INTERNAL_TAGTb4b4b4: Ta5d6ff${{Ta5d6ff Ta5d6ffsteps.tags.outputs.internalTa5d6ff Ta5d6ff}}
Tff7b72CLOSED_TAGTb4b4b4: Ta5d6ff${{Ta5d6ff Ta5d6ffsteps.tags.outputs.closedTa5d6ff Ta5d6ff}}
Tff7b72OPEN_TAGTb4b4b4: Ta5d6ff${{Ta5d6ff Ta5d6ffsteps.tags.outputs.openTa5d6ff Ta5d6ff}}
Tff7b72runTb4b4b4: Tb4b4b4|
Tff7b72# Helper: generate categorized notes via GitHub API, honoring
Tff7b72# .github/release.yml categories + excludes. Works with ANY existing
Tff7b72# tag as previous_tag (release OR channel), target = a tag or a SHA.
Tff7b72generate_notes_api() {
Tff7b72local target_sha="$1" previous_tag="$2"
Tff7b72local body
Tff7b72body=$(gh api "repos/${REPO}/releases/generate-notes" \
Tff7b72-f tag_name="unreleased" \
Tff7b72-f target_commitish="$target_sha" \
Tff7b72-f previous_tag_name="$previous_tag" \
Tff7b72--jq '.body' 2>/dev/null) || return 1

Tff7b72echo "$body" \
Tff7b72| sed '/^<!-- .* -->/d' \
Tff7b72| sed '/^## What'\''s Changed$/d' \
Tff7b72| sed '/^## New Contributors$/,$d' \
Tff7b72| sed '/^\*\*Full Changelog\*\*/d' \
Tff7b72| sed '/./,$!d' \
Tff7b72| sed -e :a -e '/^\n*$/{$d;N;ba}'
Tff7b72}

Tff7b72# Helper: demote ### headings to #### for nesting under channel sections
Tff7b72demote_headings() {
Tff7b72sed 's/^### /#### /g'
Tff7b72}

Tff7b72# Extract New Contributors from the full range (prod -> HEAD)
Tff7b72FULL_NOTES=$(gh api "repos/${REPO}/releases/generate-notes" \
Tff7b72-f tag_name="unreleased" \
Tff7b72-f target_commitish="$HEAD_SHA" \
Tff7b72-f previous_tag_name="$PROD_TAG" \
Tff7b72--jq '.body' 2>/dev/null) || true

Tff7b72CONTRIBUTORS=""
Tff7b72if echo "$FULL_NOTES" | grep -q '## New Contributors'; then
Tff7b72CONTRIBUTORS=$(echo "$FULL_NOTES" | awk '
Tff7b72/^## New Contributors$/ { capture=1 }
Tff7b72capture && /^\*\*Full Changelog\*\*/ { exit }
Tff7b72capture { print }
Tff7b72')
Tff7b72fi

Tff7b72# === Build ordered segment chain by commit ancestry ===
Tff7b72# Collect surviving channel tags with their labels and SHAs
Tff7b72declare -a TAG_LABELS=()
Tff7b72declare -a TAG_NAMES=()
Tff7b72declare -a TAG_SHAS=()

Tff7b72for LABEL_TAG in "Internal:$INTERNAL_TAG" "Closed Beta:$CLOSED_TAG" "Open Beta:$OPEN_TAG"; do
Tff7b72LABEL="${LABEL_TAG%%:*}"
Tff7b72TAG="${LABEL_TAG#*:}"
Tff7b72if [ -n "$TAG" ]; then
Tff7b72TAG_LABELS+=("$LABEL")
Tff7b72TAG_NAMES+=("$TAG")
Tff7b72TAG_SHAS+=("$(git rev-parse "$TAG")")
Tff7b72fi
Tff7b72done

Tff7b72# Sort by topological order (oldest ancestor first) using git merge-base
Tff7b72# Bubble sort is fine for <=3 elements
Tff7b72N=${#TAG_SHAS[@]}
Tff7b72for ((i=0; i<N; i++)); do
Tff7b72for ((j=i+1; j<N; j++)); do
Tff7b72# If TAG_SHAS[j] is an ancestor of TAG_SHAS[i], swap (ancestor goes first)
Tff7b72if git merge-base --is-ancestor "${TAG_SHAS[$j]}" "${TAG_SHAS[$i]}" 2>/dev/null; then
Tff7b72TMP_L="${TAG_LABELS[$i]}"; TAG_LABELS[$i]="${TAG_LABELS[$j]}"; TAG_LABELS[$j]="$TMP_L"
Tff7b72TMP_N="${TAG_NAMES[$i]}"; TAG_NAMES[$i]="${TAG_NAMES[$j]}"; TAG_NAMES[$j]="$TMP_N"
Tff7b72TMP_S="${TAG_SHAS[$i]}"; TAG_SHAS[$i]="${TAG_SHAS[$j]}"; TAG_SHAS[$j]="$TMP_S"
Tff7b72fi
Tff7b72done
Tff7b72done

Tff7b72echo "Ordered chain: prod=${PROD_TAG}"
Tff7b72for ((i=0; i<N; i++)); do
Tff7b72echo " -> ${TAG_LABELS[$i]} (${TAG_NAMES[$i]}) @ ${TAG_SHAS[$i]:0:12}"
Tff7b72done
Tff7b72echo " -> HEAD @ ${HEAD_SHA:0:12}"

Tff7b72# === Generate segments (oldest to newest, each relative to its predecessor) ===
Tff7b72SECTIONS=""
Tff7b72PREV_REF="$PROD_TAG"

Tff7b72for ((i=0; i<N; i++)); do
Tff7b72LABEL="${TAG_LABELS[$i]}"
Tff7b72TAG="${TAG_NAMES[$i]}"
Tff7b72SHA="${TAG_SHAS[$i]}"

Tff7b72NOTES=$(generate_notes_api "$SHA" "$PREV_REF" | demote_headings)

Tff7b72if [ -n "$NOTES" ]; then
Tff7b72SECTION=$(cat <<SECTION
Tff7b72### ${LABEL} (${TAG})
Tff7b72Changes since [\`${PREV_REF}\`](https://github.com/${REPO}/releases/tag/${PREV_REF}):

Tff7b72${NOTES}
Tff7b72SECTION
Tff7b72)
Tff7b72# Prepend (we want newest-first in final output)
Tff7b72if [ -n "$SECTIONS" ]; then
Tff7b72SECTIONS="${SECTION}

Tff7b72${SECTIONS}"
Tff7b72else
Tff7b72SECTIONS="$SECTION"
Tff7b72fi
Tff7b72fi

Tff7b72PREV_REF="$TAG"
Tff7b72done

Tff7b72# === Unreleased segment (HEAD -> newest tag) ===
Tff7b72UNRELEASED_SECTION=""
Tff7b72# Indexing TAG_NAMES[-1] on an empty array is a fatal error under
Tff7b72# bash -e, so the no-channel-tags state (right after a production
Tff7b72# release) needs an explicit branch instead of a :- fallback.
Tff7b72if (( N > 0 )); then
Tff7b72LATEST_TAG="${TAG_NAMES[$((N-1))]}"
Tff7b72else
Tff7b72LATEST_TAG="$PROD_TAG"
Tff7b72fi
Tff7b72if [ "$HEAD_SHA" != "$(git rev-parse "$LATEST_TAG" 2>/dev/null)" ]; then
Tff7b72UNRELEASED_NOTES=$(generate_notes_api "$HEAD_SHA" "$LATEST_TAG" | demote_headings)
Tff7b72if [ -n "$UNRELEASED_NOTES" ]; then
Tff7b72UNRELEASED_SECTION=$(cat <<SECTION
Tff7b72### Unreleased (not yet in any build)

Tff7b72${UNRELEASED_NOTES}
Tff7b72SECTION
Tff7b72)
Tff7b72fi
Tff7b72fi

Tff7b72# Assemble the full unreleased section (newest first).
Tff7b72# Use if-statements, not `[ -n ... ] && ...` lists: a failed test as
Tff7b72# the group's last command makes the whole step exit 1 under bash -e.
Tff7b72{
Tff7b72echo "## [Unreleased]"
Tff7b72echo ""
Tff7b72if [ -n "$UNRELEASED_SECTION" ]; then echo "$UNRELEASED_SECTION"; echo ""; fi
Tff7b72if [ -n "$SECTIONS" ]; then echo "$SECTIONS"; echo ""; fi
Tff7b72if [ -n "$CONTRIBUTORS" ]; then echo "$CONTRIBUTORS"; fi
Tff7b72} > /tmp/unreleased-section.md

Tb4b4b4- Tff7b72nameTb4b4b4: Ta5d6ffUpdateTa5d6ff Ta5d6ffCHANGELOG.md
Tff7b72ifTb4b4b4: Ta5d6ffsteps.tags.outputs.prodTa5d6ff Ta5d6ff!=Ta5d6ff Ta5d6ff''
Tff7b72idTb4b4b4: Ta5d6ffupdate
Tff7b72runTb4b4b4: Tb4b4b4|
Tff7b72SECTION=$(cat /tmp/unreleased-section.md)

Tff7b72python3 -c "
Tff7b72import sys
Tff7b72start_marker = '<!-- UNRELEASED_START -->'
Tff7b72end_marker = '<!-- UNRELEASED_END -->'
Tff7b72section = sys.argv[1]
Tff7b72with open('CHANGELOG.md', 'r') as f:
Tff7b72content = f.read()
Tff7b72start = content.find(start_marker)
Tff7b72end = content.find(end_marker)
Tff7b72if start == -1 or end == -1:
Tff7b72print('Markers not found in CHANGELOG.md')
Tff7b72sys.exit(1)
Tff7b72new_content = content[:start + len(start_marker)] + '\n' + section + '\n' + content[end:]
Tff7b72with open('CHANGELOG.md', 'w') as f:
Tff7b72f.write(new_content)
Tff7b72" "$SECTION"

Tff7b72if git diff --quiet CHANGELOG.md; then
Tff7b72echo "changed=false" >> "$GITHUB_OUTPUT"
Tff7b72else
Tff7b72echo "changed=true" >> "$GITHUB_OUTPUT"
Tff7b72fi

Tb4b4b4- Tff7b72nameTb4b4b4: Ta5d6ffCreateTa5d6ff Ta5d6fforTa5d6ff Ta5d6ffupdateTa5d6ff Ta5d6ffchangelogTa5d6ff Ta5d6ffPR
Tff7b72ifTb4b4b4: Ta5d6ffsteps.tags.outputs.prodTa5d6ff Ta5d6ff!=Ta5d6ff Ta5d6ff''Ta5d6ff Ta5d6ff&&Ta5d6ff Ta5d6ffsteps.update.outputs.changedTa5d6ff Ta5d6ff==Ta5d6ff Ta5d6ff'true'
Tff7b72envTb4b4b4:
Tff7b72GH_TOKENTb4b4b4: Ta5d6ff${{Ta5d6ff Ta5d6ffgithub.tokenTa5d6ff Ta5d6ff}}
Tff7b72runTb4b4b4: Tb4b4b4|
Tff7b72BRANCH="automation/update-changelog"

Tff7b72git config user.name "github-actions[bot]"
Tff7b72git config user.email "github-actions[bot]@users.noreply.github.com"

Tff7b72# Force-update the automation branch
Tff7b72git checkout -B "$BRANCH"
Tff7b72git add CHANGELOG.md
Tff7b72git commit -m "docs: update CHANGELOG.md"
Tff7b72git push origin "$BRANCH" --force

Tff7b72# Create or update the PR
Tff7b72EXISTING_PR=$(gh pr list --head "$BRANCH" --state open --json number -q '.[0].number')
Tff7b72if [ -n "$EXISTING_PR" ]; then
Tff7b72echo "Updated existing PR #$EXISTING_PR"
Tff7b72else
Tff7b72gh pr create \
Tff7b72--title "docs: update CHANGELOG.md" \
Tff7b72--body "Automated changelog update from push to main." \
Tff7b72--head "$BRANCH" \
Tff7b72--base main \
Tff7b72--label "automation" \
Tff7b72--label "skip-changelog"
Tff7b72echo "Created new changelog PR"
Tff7b72fi

Tff7b72# Post the required "Check Workflow Status" commit status so the PR
Tff7b72# isn't blocked. PRs from GITHUB_TOKEN don't trigger pull_request
Tff7b72# workflows, so the normal CI never runs. CHANGELOG-only PRs don't
Tff7b72# need CI checks.
Tff7b72COMMIT_SHA=$(git rev-parse HEAD)
Tff7b72gh api "repos/${{ github.repository }}/statuses/${COMMIT_SHA}" \
Tff7b72-f state="success" \
Tff7b72-f context="Check Workflow Status" \
Tff7b72-f description="Skipped — changelog-only PR"

Served by rngit 1.5.2 - Generated in 0.08s